﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.Serialization;

namespace SmartRoute.Data
{
    [Serializable]
    /// <summary>
    /// An immutable location that can be used by routing services.
    /// </summary>
    public class Location: IComparable<Location>, ISerializable
    {
        
        #region Private Fields

        /// <summary>
        /// The immutable latitude coordinate for this location.
        /// </summary>
        protected double _latitude;

        /// <summary>
        /// The immutable longtitude coordinate for this location.
        /// </summary>
        protected double _longitude;

        /// <summary>
        /// The address to allow this to be pretty-printed.
        /// </summary>
        protected string _address;

        #endregion

        #region Accessors

        /// <summary>
        /// The longtitude of this location.
        /// </summary>
        public double Longitude
        {
            get { return _longitude; }
        }

        /// <summary>
        /// The latitude of this location.
        /// </summary>
        public double Latitude
        {
            get { return _latitude; }
        }

        /// <summary>
        /// The address of this location. Null if undefined.
        /// </summary>
        public string Address
        {
            get
            {
                return _address;
            }
            set
            {
                _address = value;
            }
        }

        /// <summary>
        /// Returns the coordinates in a string format (latitude,longtitude).
        /// </summary>
        public string CoordinatesAsString
        {
            get
            {
                return String.Format("({0},{1})", this.Latitude, this.Longitude);
            }
        }

        /// <summary>
        /// Returns the coordinates if an address is not set, otherwise returns the address.
        /// </summary>
        public override string ToString()
        {
            return this.Address + this.CoordinatesAsString;
        }

        #endregion

        #region Constructors

        /// <summary>
        /// Builds a new location object with given latitude, longtitude, and address.
        /// </summary>
        /// <param name="latitude">The immutable latitude of this location.</param>
        /// <param name="longtitude">The immutable longtitude of this location.</param>
        /// <param name="address">The address for this location.</param>
        public Location(double latitude, double longtitude, string address)
        {
            _latitude = latitude;
            _longitude = longtitude;
            if(address != null && address.Length > 0) _address = address;
            else _address = "";
        }

        /// <summary>
        /// Builds a location with a default address which shows the coordinates.
        /// </summary>
        /// <param name="latitude">The latitude of this location.</param>
        /// <param name="longtitude">The longtitude of this location.</param>
        /// <returns>A Location object with coordinates as its address field.</returns>
        public static Location WithoutAddress(double latitude, double longtitude)
        {
            return new Location(latitude, longtitude, "");
        }

        /// <summary>
        /// Protected nullary constructor to permit extension of the class.
        /// </summary>
        protected Location()
        {
        }

        #endregion

        #region Comparison

        /// <summary>
        /// Compares two location objects by coordinates and address.
        /// Latitude is compared first. If the latitudes are equal, longtitudes are compared.
        /// Address is compared last.
        /// </summary>
        /// <param name="other">The location to compare to this one.</param>
        /// <returns>0 only if the latitude, longtitude, and address are exactly the same.</returns>
        public int CompareTo(Location other)
        {
            var latDiff = this.Latitude.CompareTo(other.Latitude);
            if (latDiff != 0) return latDiff;
            var longDiff = this.Longitude.CompareTo(other.Longitude);
            return longDiff;
        }

        /// <summary>
        /// True if the given object represents the same location.
        /// </summary>
        /// <param name="obj">The object to be compared to this location.</param>
        /// <returns>True if coordinates and addresses are the same. False otherwise.</returns>
        public override bool Equals(object obj)
        {
            return obj is Location && 0 == this.CompareTo((Location)obj);
        }

        //Use only immutable values to provide the hash code.
        public override int GetHashCode()
        {
            unchecked
            {
                //Jenkins hash function
                int hash = this.Latitude.GetHashCode();
                hash += (hash << 10);
                hash ^= (hash >> 6);
                hash += this.Longitude.GetHashCode();
                hash += (hash << 10);
                hash ^= (hash >> 6);
                hash += (hash << 3);
                hash ^= (hash >> 11);
                hash += (hash << 15);
                return hash;
            }
        }

        #endregion

        #region Functions

        /// <summary>
        /// Returns a location at the same coordinates with the given address.
        /// </summary>
        /// <param name="newAddress">The address to give the new location.</param>
        /// <returns>A location with a changed address.</returns>
        public Location changeAddress(string newAddress)
        {
            return new Location(this.Latitude, this.Longitude, newAddress);
        }

        public Location changeCoordinates(double latitude, double longitude)
        {
            return new Location(latitude, longitude, this._address);
        }

        public Location changeLatitude(double latitude)
        {
            return this.changeCoordinates(latitude, this.Longitude);
        }

        public Location changeLongitude(double longitude)
        {
            return this.changeCoordinates(this.Latitude, longitude);
        }

        #endregion

        #region Operator Overloads

        //Overload the == operator
        public static bool operator==(Location first, Location second)
        {
            return first.Equals(second);
        }

        //Overload the != operator
        public static bool operator !=(Location first, Location second)
        {
            return !(first == second);
        }

        #endregion

        #region ISerializable Members

        public void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            info.AddValue("Latitude", Latitude);
            info.AddValue("Longitude", Longitude);
            info.AddValue("Address", Address);
        }

        private Location(SerializationInfo info, StreamingContext context)
        {
            this._latitude = info.GetDouble("Latitude");
            this._longitude = info.GetDouble("Longitude");
            this._address = info.GetString("Address");
        }

        #endregion
    }

}
